
/*-------------------------------------------------------------------------
 *
 * xlogutils.c
 *
 * PostgreSQL 写前日志管理器实用程序例程
 *
 * 本文件包含由 XLOG 重放函数使用的支持例程。
 * 这些代码在系统正常运行期间并不会使用。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * src/backend/access/transam/xlogutils.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <unistd.h>

#include "access/timeline.h"
#include "access/xlogrecovery.h"
#include "access/xlog_internal.h"
#include "access/xlogprefetcher.h"
#include "access/xlogutils.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "storage/fd.h"
#include "storage/smgr.h"
#include "utils/guc.h"
#include "utils/hsearch.h"
#include "utils/rel.h"


/* GUC 变量 */
bool		ignore_invalid_pages = false;

/*
 * 我们是在从 XLOG 中恢复吗？
 *
 * 这只有在启动过程中才为真；应理解为
 * “该进程正在重放 WAL 记录”，而不是“系统处于
 * 恢复模式”。它应该主要由需要在 WAL 重做函数中
 * 不同处理的函数来检查（例如，跳过 WAL
 * 日志记录）。要检查系统是否处于恢复状态，无论
 * 您在哪个进程中运行，请使用 RecoveryInProgress()，
 * 但仅在共享内存启动和锁初始化之后。
 *
 * 这来自 xlog.c 和 xlogrecovery.c，但在这里是因为
 * 它主要被 WAL 重做函数读取。
 */
bool		InRecovery = false;

/* 我们在热备份模式吗？仅在启动过程中有效，请参见 xlogutils.h */
HotStandbyState standbyState = STANDBY_DISABLED;

/*
 * 在 XLOG 重播过程中，我们可能会看到某些页面的增量更新
 * 的 XLOG 记录，而这些页面已不存在，因为它们的关系随后被删除或
 * 截断。 （注意：这仅在 full_page_writes = OFF 时可能，
 * 因为当它为 ON 时，我们看到的对页面的第一次引用应始终
 * 是完全页面重写，而不是增量更新。）我们并不是简单地
 * 忽略这些记录，而是记录下被引用的页面，如果稍后在重播
 * 中没有看到覆盖该页面的删除或截断，则会抱怨。
 */
typedef struct xl_invalid_page_key
{
	RelFileNode node;			/* 关系 */
	ForkNumber	forkno;			/* 分支编号 */
	BlockNumber blkno;			/* 页面 */
} xl_invalid_page_key;

typedef struct xl_invalid_page
{
	xl_invalid_page_key key;	/* 哈希键 ... 必须在前面 */
	bool		present;		/* 页面存在但包含零 */
} xl_invalid_page;

static HTAB *invalid_page_tab = NULL;

static int	fc_read_local_xlog_page_guts(XLogReaderState *fc_state, XLogRecPtr fc_targetPagePtr,
									  int fc_reqLen, XLogRecPtr fc_targetRecPtr,
									  char *fc_cur_page, bool fc_wait_for_wal);

/* 报告对无效页面的引用 */
static void fc_report_invalid_page(int fc_elevel, RelFileNode fc_node, ForkNumber fc_forkno,
					BlockNumber fc_blkno, bool fc_present)
{
	char	   *fc_path = relpathperm(fc_node, fc_forkno);

	if (fc_present)
		elog(fc_elevel, "page %u of relation %s is uninitialized",
			 fc_blkno, fc_path);
	else
		elog(fc_elevel, "page %u of relation %s does not exist",
			 fc_blkno, fc_path);
	pfree(fc_path);
}

/* 记录对无效页面的引用 */
static void fc_log_invalid_page(RelFileNode fc_node, ForkNumber fc_forkno, BlockNumber fc_blkno,
				 bool fc_present)
{
	xl_invalid_page_key fc_key;
	xl_invalid_page *fc_hentry;
	bool		fc_found;

	/*
	 * 一旦恢复到一致状态，无效页面表应该为空并保持为空。
	 * 如果在达到一致性后发现对无效页面的引用，立即 PANIC。
	 * 这可能显得激进，但比让无效引用在哈希表中停留
	 * 直到恢复结束并在那里 PANIC 更好，如果这是一个
	 * 备用服务器，这可能会在很久以后才发生。
	 */
	if (reachedConsistency)
	{
		fc_report_invalid_page(WARNING, fc_node, fc_forkno, fc_blkno, fc_present);
		elog(ignore_invalid_pages ? WARNING : PANIC,
			 "WAL contains references to invalid pages");
	}

	/*
	 * 在 DEBUG1 级别记录对无效页面的引用。
	 * 这允许对原因进行一些跟踪（注意 elog 上下文机制
	 * 将告诉我们有关生成引用的 XLOG 记录的一些信息）。
	 */
	if (message_level_is_interesting(DEBUG1))
		fc_report_invalid_page(DEBUG1, fc_node, fc_forkno, fc_blkno, fc_present);

	if (invalid_page_tab == NULL)
	{
		/* 在首次需要时创建哈希表 */
		HASHCTL		fc_ctl;

		fc_ctl.keysize = sizeof(xl_invalid_page_key);
		fc_ctl.entrysize = sizeof(xl_invalid_page);

		invalid_page_tab = hash_create("XLOG invalid-page table",
									   100,
									   &fc_ctl,
									   HASH_ELEM | HASH_BLOBS);
	}

	/* 我们目前假设 xl_invalid_page_key 不包含填充 */
	fc_key.node = fc_node;
	fc_key.forkno = fc_forkno;
	fc_key.blkno = fc_blkno;
	fc_hentry = (xl_invalid_page *)
		hash_search(invalid_page_tab, (void *) &fc_key, HASH_ENTER, &fc_found);

	if (!fc_found)
	{
		/* hash_search 已经填充了键 */
		fc_hentry->present = fc_present;
	}
	else
	{
		/* 重复引用 ... 保持“存在”不变 */
	}
}

/* 忘记任何无效页面 >= minblkno，因为它们已被删除 */
static void fc_forget_invalid_pages(RelFileNode fc_node, ForkNumber fc_forkno, BlockNumber fc_minblkno)
{
	HASH_SEQ_STATUS fc_status;
	xl_invalid_page *fc_hentry;

	if (invalid_page_tab == NULL)
		return;					/* 无需操作 */

	hash_seq_init(&fc_status, invalid_page_tab);

	while ((fc_hentry = (xl_invalid_page *) hash_seq_search(&fc_status)) != NULL)
	{
		if (RelFileNodeEquals(fc_hentry->key.node, fc_node) &&
			fc_hentry->key.forkno == fc_forkno &&
			fc_hentry->key.blkno >= fc_minblkno)
		{
			if (message_level_is_interesting(DEBUG2))
			{
				char	   *fc_path = relpathperm(fc_hentry->key.node, fc_forkno);

				elog(DEBUG2, "page %u of relation %s has been dropped",
					 fc_hentry->key.blkno, fc_path);
				pfree(fc_path);
			}

			if (hash_search(invalid_page_tab,
							(void *) &fc_hentry->key,
							HASH_REMOVE, NULL) == NULL)
				elog(ERROR, "hash table corrupted");
		}
	}
}

/* 忘记整个数据库中任何无效页面 */
static void fc_forget_invalid_pages_db(Oid fc_dbid)
{
	HASH_SEQ_STATUS fc_status;
	xl_invalid_page *fc_hentry;

	if (invalid_page_tab == NULL)
		return;					/* 无需操作 */

	hash_seq_init(&fc_status, invalid_page_tab);

	while ((fc_hentry = (xl_invalid_page *) hash_seq_search(&fc_status)) != NULL)
	{
		if (fc_hentry->key.node.dbNode == fc_dbid)
		{
			if (message_level_is_interesting(DEBUG2))
			{
				char	   *fc_path = relpathperm(fc_hentry->key.node, fc_hentry->key.forkno);

				elog(DEBUG2, "page %u of relation %s has been dropped",
					 fc_hentry->key.blkno, fc_path);
				pfree(fc_path);
			}

			if (hash_search(invalid_page_tab,
							(void *) &fc_hentry->key,
							HASH_REMOVE, NULL) == NULL)
				elog(ERROR, "hash table corrupted");
		}
	}
}

/* 是否有任何对无效页面的未解决引用？ */
bool XLogHaveInvalidPages(void)
{
	if (invalid_page_tab != NULL &&
		hash_get_num_entries(invalid_page_tab) > 0)
		return true;
	return false;
}

/* 抱怨任何剩余的无效页面条目 */
void XLogCheckInvalidPages(void)
{
	HASH_SEQ_STATUS fc_status;
	xl_invalid_page *fc_hentry;
	bool		fc_foundone = false;

	if (invalid_page_tab == NULL)
		return;					/* 无需操作 */

	hash_seq_init(&fc_status, invalid_page_tab);

	/*
	 * 我们的策略是对所有剩余条目发出 WARNING 消息，
	 * 仅在转储所有可用信息后进行 PANIC。
	 */
	while ((fc_hentry = (xl_invalid_page *) hash_seq_search(&fc_status)) != NULL)
	{
		fc_report_invalid_page(WARNING, fc_hentry->key.node, fc_hentry->key.forkno,
							fc_hentry->key.blkno, fc_hentry->present);
		fc_foundone = true;
	}

	if (fc_foundone)
		elog(ignore_invalid_pages ? WARNING : PANIC,
			 "WAL contains references to invalid pages");

	hash_destroy(invalid_page_tab);
	invalid_page_tab = NULL;
}


/*
 * XLogReadBufferForRedo
 *		在 XLOG 重放期间读取页面
 *
 * 从 WAL 记录中读取一个块到共享缓冲区缓存，并确定需要进行什么操作以重做对其的更改。如果 WAL 记录包含页面的完整图像，它将被恢复。
 *
 * 'record.EndRecPtr' 与页面的 LSN 进行比较，以确定该记录是否已经被重放。'block_id' 是创建 WAL 记录时块注册的 ID 号码。
 *
 * 返回以下之一：
 *
 *	BLK_NEEDS_REDO	- 需要应用 WAL 记录中的更改
 *	BLK_DONE		- 块不需要重放
 *	BLK_RESTORED	- 块是从记录中包含的完整页面图像中恢复的
 *	BLK_NOTFOUND	- 块未找到（因为它在 WAL 流中的后续操作中被截断）
 *
 * 返回时，缓冲区以独占模式锁定，并返回给 *buf。请注意，即使不需要重放，缓冲区也会被锁定并返回。（在单进程崩溃恢复期间获取缓冲区锁实际上不是必需的，但有些子例程，例如 MarkBufferDirty，如果我们没有锁会抱怨。在热备用模式下，这绝对是必要的。）
 *
 * 注意：当 XLOG 中的备份块可用且设置了 BKPIMAGE_APPLY 标志时，我们会恢复它，即使数据库中的页面显示更新。这是为了保护我们免受在崩溃期间部分或不正确写入的数据库页面的影响。我们假设 XLOG 数据必须是好的，因为它通过了 CRC 检查，而数据库页面可能不是。这将迫使我们重放 XLOG 中出现的页面的所有后续修改，而不是可能忽略它们作为已应用，但这并不是一个巨大缺点。
 */
XLogRedoAction
XLogReadBufferForRedo(XLogReaderState *fc_record, uint8 fc_block_id,
					  Buffer *fc_buf)
{
	return XLogReadBufferForRedoExtended(fc_record, fc_block_id, RBM_NORMAL,
										 false, fc_buf);
}

/*
 * 针对 WAL 记录引用的缓冲区进行锁定和钉住，以便重新初始化。
 */
Buffer XLogInitBufferForRedo(XLogReaderState *fc_record, uint8 fc_block_id)
{
	Buffer		fc_buf;

	XLogReadBufferForRedoExtended(fc_record, fc_block_id, RBM_ZERO_AND_LOCK, false,
								  &fc_buf);
	return fc_buf;
}

/*
 * XLogReadBufferForRedoExtended
 *		类似于 XLogReadBufferForRedo，但带有额外选项。
 *
 * 在 RBM_ZERO_* 模式下，如果页面不存在，则以全零页面扩展关系直到引用的块号。在 RBM_ZERO_AND_LOCK 和 RBM_ZERO_AND_CLEANUP_LOCK 模式下，返回值始终为 BLK_NEEDS_REDO。
 *
 * （RBM_ZERO_AND_CLEANUP_LOCK 模式与 get_cleanup_lock 参数是冗余的。请勿使用不一致的组合！）
 *
 * 如果 'get_cleanup_lock' 为真，则使用 LockBufferForCleanup() 在缓冲区上获取“清理锁”，而不是常规独占锁。
 */
XLogRedoAction
XLogReadBufferForRedoExtended(XLogReaderState *fc_record,
							  uint8 fc_block_id,
							  ReadBufferMode fc_mode, bool fc_get_cleanup_lock,
							  Buffer *fc_buf)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	RelFileNode fc_rnode;
	ForkNumber	fc_forknum;
	BlockNumber fc_blkno;
	Buffer		fc_prefetch_buffer;
	Page		fc_page;
	bool		fc_zeromode;
	bool		fc_willinit;

	if (!XLogRecGetBlockTagExtended(fc_record, fc_block_id, &fc_rnode, &fc_forknum, &fc_blkno,
									&fc_prefetch_buffer))
	{
		/* 调用者指定了一个虚假的 block_id */
		elog(PANIC, "failed to locate backup block with ID %d in WAL record",
			 fc_block_id);
	}

	/*
	 * 确保如果块被标记为 WILL_INIT，调用者将要初始化它。反之亦然。
	 */
	fc_zeromode = (fc_mode == RBM_ZERO_AND_LOCK || fc_mode == RBM_ZERO_AND_CLEANUP_LOCK);
	fc_willinit = (XLogRecGetBlock(fc_record, fc_block_id)->flags & BKPBLOCK_WILL_INIT) != 0;
	if (fc_willinit && !fc_zeromode)
		elog(PANIC, "block with WILL_INIT flag in WAL record must be zeroed by redo routine");
	if (!fc_willinit && fc_zeromode)
		elog(PANIC, "block to be initialized in redo routine must be marked with WILL_INIT flag in the WAL record");

	/* 如果它具有完整页面图像并且应该被恢复，则执行恢复。 */
	if (XLogRecBlockImageApply(fc_record, fc_block_id))
	{
		Assert(XLogRecHasBlockImage(fc_record, fc_block_id));
		*fc_buf = XLogReadBufferExtended(fc_rnode, fc_forknum, fc_blkno,
									  fc_get_cleanup_lock ? RBM_ZERO_AND_CLEANUP_LOCK : RBM_ZERO_AND_LOCK,
									  fc_prefetch_buffer);
		fc_page = BufferGetPage(*fc_buf);
		if (!RestoreBlockImage(fc_record, fc_block_id, fc_page))
			ereport(ERROR,
					(errcode(ERRCODE_INTERNAL_ERROR),
					 errmsg_internal("%s", fc_record->errormsg_buf)));

		/*
		 * 页面可能未初始化。如果是这样，我们无法设置 LSN，因为这会损坏页面。
		 */
		if (!PageIsNew(fc_page))
		{
			PageSetLSN(fc_page, fc_lsn);
		}

		MarkBufferDirty(*fc_buf);

		/*
		 * 在崩溃恢复结束时，未经日志记录的关系的初始化分支
		 * 被复制，而不通过共享缓冲区。因此我们需要
		 * 强制初始化分支的磁盘状态始终与
		 * 共享缓冲区中的状态同步。
		 */
		if (fc_forknum == INIT_FORKNUM)
			FlushOneBuffer(*fc_buf);

		return BLK_RESTORED;
	}
	else
	{
		*fc_buf = XLogReadBufferExtended(fc_rnode, fc_forknum, fc_blkno, fc_mode, fc_prefetch_buffer);
		if (BufferIsValid(*fc_buf))
		{
			if (fc_mode != RBM_ZERO_AND_LOCK && fc_mode != RBM_ZERO_AND_CLEANUP_LOCK)
			{
				if (fc_get_cleanup_lock)
					LockBufferForCleanup(*fc_buf);
				else
					LockBuffer(*fc_buf, BUFFER_LOCK_EXCLUSIVE);
			}
			if (fc_lsn <= PageGetLSN(BufferGetPage(*fc_buf)))
				return BLK_DONE;
			else
				return BLK_NEEDS_REDO;
		}
		else
			return BLK_NOTFOUND;
	}
}

/*
 * XLogReadBufferExtended
 *		在XLOG重放期间读取页面
 *
 * 这在功能上与ReadBufferExtended相当。关于“模式”参数的行为有一些
 * 不同：
 *
 * 在RBM_NORMAL模式下，如果页面不存在，或包含全零，我们
 * 返回InvalidBuffer。在这种情况下，调用者应该静默跳过此页面的
 * 更新。（在这种情况下，我们预期该页面后来被删除或截断。如果我们在
 * WAL序列中未看到相关证据，我们将在WAL重放结束时抱怨。）
 *
 * 在RBM_ZERO_*模式下，如果页面不存在，则关系以全零页面扩展
 * 直到给定的块号。
 *
 * 在RBM_NORMAL_NO_LOG模式下，如果页面不存在，我们返回InvalidBuffer，
 * 并且不检查全零。因此，不会记录日志条目以暗示该页面应该在后面
 * 被删除或截断。
 *
 * 可选地，recent_buffer可以用来提供关于页面在缓冲池中的位置的提示；
 * 它不必是正确的，但如果是，则避免了缓冲映射表的探测。
 *
 * 注意：重做函数通常不应直接调用此函数。要获取要修改的页面，
 * 使用XLogReadBufferForRedoExtended。重要的是，所有由WAL记录修改的
 * 页面都必须在WAL记录中注册，否则它们对需要知道哪些页面被修改的工具
 * 将是不可见的。
 */
Buffer XLogReadBufferExtended(RelFileNode fc_rnode, ForkNumber fc_forknum,
					   BlockNumber fc_blkno, ReadBufferMode fc_mode,
					   Buffer fc_recent_buffer)
{
	BlockNumber fc_lastblock;
	Buffer		fc_buffer;
	SMgrRelation fc_smgr;

	Assert(fc_blkno != P_NEW);

	/* 我们是否有线索表明缓冲区可能已经存在？ */
	if (BufferIsValid(fc_recent_buffer) &&
		fc_mode == RBM_NORMAL &&
		ReadRecentBuffer(fc_rnode, fc_forknum, fc_blkno, fc_recent_buffer))
	{
		fc_buffer = fc_recent_buffer;
		goto recent_buffer_fast_path;
	}

	/* 在smgr级别打开关系 */
	fc_smgr = smgropen(fc_rnode, InvalidBackendId);

	/*
	 * 如果目标文件不存在，则创建它。这让我们能够处理
	 * 如果重放序列包含写入后被删除的关系的情况。
	 * （该例程的原始编码将抑制写入，但这似乎有风险
	 * 丢失宝贵的数据，如果在崩溃期间文件系统丢失了inode。
	 * 直到我们实际被告知删除文件，最好是写入数据。）
	 */
	smgrcreate(fc_smgr, fc_forknum, true);

	fc_lastblock = smgrnblocks(fc_smgr, fc_forknum);

	if (fc_blkno < fc_lastblock)
	{
		/* 页面存在于文件中 */
		fc_buffer = ReadBufferWithoutRelcache(fc_rnode, fc_forknum, fc_blkno,
										   fc_mode, NULL, true);
	}
	else
	{
		/* 嗯，页面在文件中不存在 */
		if (fc_mode == RBM_NORMAL)
		{
			fc_log_invalid_page(fc_rnode, fc_forknum, fc_blkno, false);
			return InvalidBuffer;
		}
		if (fc_mode == RBM_NORMAL_NO_LOG)
			return InvalidBuffer;
		/* 扩展文件是可以的 */
		/* 我们只在恢复中这样做 - 不需要关系扩展锁 */
		Assert(InRecovery);
		fc_buffer = InvalidBuffer;
		do
		{
			if (fc_buffer != InvalidBuffer)
			{
				if (fc_mode == RBM_ZERO_AND_LOCK || fc_mode == RBM_ZERO_AND_CLEANUP_LOCK)
					LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);
				ReleaseBuffer(fc_buffer);
			}
			fc_buffer = ReadBufferWithoutRelcache(fc_rnode, fc_forknum,
											   P_NEW, fc_mode, NULL, true);
		}
		while (BufferGetBlockNumber(fc_buffer) < fc_blkno);
		/* 处理P_NEW返回非连续页面的边缘情况 */
		if (BufferGetBlockNumber(fc_buffer) != fc_blkno)
		{
			if (fc_mode == RBM_ZERO_AND_LOCK || fc_mode == RBM_ZERO_AND_CLEANUP_LOCK)
				LockBuffer(fc_buffer, BUFFER_LOCK_UNLOCK);
			ReleaseBuffer(fc_buffer);
			fc_buffer = ReadBufferWithoutRelcache(fc_rnode, fc_forknum, fc_blkno,
											   fc_mode, NULL, true);
		}
	}

recent_buffer_fast_path:
	if (fc_mode == RBM_NORMAL)
	{
		/* 检查页面是否已初始化 */
		Page		fc_page = (Page) BufferGetPage(fc_buffer);

		/*
		 * 我们假设在没有锁的情况下PageIsNew是安全的。在恢复期间，
		 * 应该没有其他后端可以同时修改缓冲区。
		 */
		if (PageIsNew(fc_page))
		{
			ReleaseBuffer(fc_buffer);
			fc_log_invalid_page(fc_rnode, fc_forknum, fc_blkno, true);
			return InvalidBuffer;
		}
	}

	return fc_buffer;
}

/*
 * 实际上由CreateFakeRelcacheEntry返回的结构，尽管声明的
 * 返回类型是Relation。
 */
typedef struct
{
	RelationData reldata;		/* 注意：这必须是第一个 */
	FormData_pg_class pgc;
} FakeRelCacheEntryData;

typedef FakeRelCacheEntryData *FakeRelCacheEntry;

/*
 * 创建一个物理关系的伪关系缓存条目
 *
 * 在XLOG重放中使用与主代码路径相同的函数通常很方便，但这些函数通常与关系缓存条目一起工作。
 * 我们在XLOG重放期间没有有效的关系缓存，但这个函数可以用来创建一个假的关系缓存条目。
 * 仅初始化与物理存储相关的字段，如rd_rel，因此假的条目仅可在低级操作如ReadBuffer()中使用。
 *
 * 这也用于同步WAL跳过的文件。
 *
 * 调用者必须使用FreeFakeRelcacheEntry()释放返回的条目。
 */
Relation CreateFakeRelcacheEntry(RelFileNode fc_rnode)
{
	FakeRelCacheEntry fc_fakeentry;
	Relation	fc_rel;

	/* 在一个块中分配Relation结构和所有相关空间。 */
	fc_fakeentry = palloc0(sizeof(FakeRelCacheEntryData));
	fc_rel = (Relation) fc_fakeentry;

	fc_rel->rd_rel = &fc_fakeentry->pgc;
	fc_rel->rd_node = fc_rnode;

	/*
	 * 在恢复或同步WAL跳过的文件期间，我们永远不会处理临时关系。
	 */
	fc_rel->rd_backend = InvalidBackendId;

	/* 这里必须是一个永久表 */
	fc_rel->rd_rel->relpersistence = RELPERSISTENCE_PERMANENT;

	/* 我们不知道关系的名称；使用relfilenode代替 */
	sprintf(RelationGetRelationName(fc_rel), "%u", fc_rnode.relNode);

	/*
	 * 我们设置lockRelId，以防任何试图锁定虚拟关系的操作。
	 * 注意，这实际上是相当虚假的，因为relNode可能与关系的OID不同。
	 * 不过，这实际上并不重要。在恢复期间，我们是独立运行的，无法发生任何锁定冲突。
	 * 在同步期间，我们已经持有AccessExclusiveLock。
	 */
	fc_rel->rd_lockInfo.lockRelId.dbId = fc_rnode.dbNode;
	fc_rel->rd_lockInfo.lockRelId.relId = fc_rnode.relNode;

	fc_rel->rd_smgr = NULL;

	return fc_rel;
}

/*
 * 释放一个伪关系缓存条目。
 */
void FreeFakeRelcacheEntry(Relation fc_fakerel)
{
	/* 确保fakerel不再被SmgrRelation引用 */
	if (fc_fakerel->rd_smgr != NULL)
		smgrclearowner(&fc_fakerel->rd_smgr, fc_fakerel->rd_smgr);
	pfree(fc_fakerel);
}

/*
 * 在XLOG重放期间删除一个关系
 *
 * 当关系即将被删除时调用；我们需要移除与该关系的任何开放“无效页面”记录。
 */
void XLogDropRelation(RelFileNode fc_rnode, ForkNumber fc_forknum)
{
	fc_forget_invalid_pages(fc_rnode, fc_forknum, 0);
}

/*
 * 在XLOG重放期间删除整个数据库
 *
 * 和上面类似，但用于DROP DATABASE而不是删除单个关系
 */
void XLogDropDatabase(Oid fc_dbid)
{
	/*
	 * 这不必要地繁琐，因为它将关闭其他数据库的SMgrRelation对象。
	 * DROP DATABASE发生得相对较少，因此为此目的引入smgrclose的变体是没有必要的。
	 * XXX：我们是否应该让smgr条目悬空？
	 */
	smgrcloseall();

	fc_forget_invalid_pages_db(fc_dbid);
}

/*
 * 在XLOG重放期间截断关系
 *
 * 我们需要清理被删除页面的任何开放“无效页面”记录。
 */
void XLogTruncateRelation(RelFileNode fc_rnode, ForkNumber fc_forkNum,
					 BlockNumber fc_nblocks)
{
	fc_forget_invalid_pages(fc_rnode, fc_forkNum, fc_nblocks);
}

/*
 * 确定从哪个时间线读取 xlog 页面，并将
 * XLogReaderState 的 currTLI 设置为该时间线 ID。
 *
 * 当我们可能正在读取在提升之前生成的 xlog 时，我们关心 xlogreader 中的时间线，无论我们当前是恢复中的备用机还是提升后的主机在阅读旧主机生成的 xlog。
 *
 * wantPage 必须设置为要读取的页面的起始地址，而 wantLength 必须设置为将要读取的页面的大小，最多可达 XLOG_BLCKSZ。如果要读取的大小未知，请传递 XLOG_BLCKSZ。
 *
 * currTLI 参数应该是系统范围内的当前时间线。
 * 请注意，这可能与 state->currTLI 不同，后者是调用者当前正在读取以前 xlog 记录的时间线。
 *
 * 当在历史时间线下时，只要我们到达包含时间线切换的 xlog 段的起始位置，就会急切地切换到新的时间线的 xlog 段。 服务器将该段复制到新的时间线，因此在切换点之前的所有数据都是相同的，但旧段可能不再存在。 它可能已经被删除或重命名为 .partial 后缀，因此尽管 tliSwitchPoint 表示可以，但我们不一定能继续从旧 TLI 读取。
 *
 * 我们不能仅在与上一页不同的段上读取页面时检查时间线。 我们可能已经从级联上游收到时间线切换，因此当前段突然结束（可能被重命名为 .partial），我们必须切换到新的段。 即使在读取页面的过程中，我们也可能不得不丢弃缓存的页面并切换到新的 TLI。
 *
 * 因此，调用者不能假设 currTLI 是将出现在页面的 xlp_tli 中的时间线；该页面可能始于较旧的时间线，或者我们可能在新的时间线段上从历史时间线数据中读取。
 *
 * 如果在恢复中执行，调用者还必须确保不在当前重播位置（使用 GetXLogReplayRecPtr）之后读取，以免未能注意到当前时间线变成历史时间线。
 */
void XLogReadDetermineTimeline(XLogReaderState *fc_state, XLogRecPtr fc_wantPage,
						  uint32 fc_wantLength, TimeLineID fc_currTLI)
{
	const XLogRecPtr fc_lastReadPage = (fc_state->seg.ws_segno *
									 fc_state->segcxt.ws_segsize + fc_state->segoff);

	Assert(fc_wantPage != InvalidXLogRecPtr && fc_wantPage % XLOG_BLCKSZ == 0);
	Assert(fc_wantLength <= XLOG_BLCKSZ);
	Assert(fc_state->readLen == 0 || fc_state->readLen <= XLOG_BLCKSZ);
	Assert(fc_currTLI != 0);

	/*
	 * 如果所需页面当前已被读取并且有效，我们就没有什么好做的。
	 *
	 * 调用者应该确保它没有将 readOff 之前推进到该时间线的有效限制之外，因此当前的 TLI 是否已经变为历史时间线并不重要。
	 */
	if (fc_lastReadPage == fc_wantPage &&
		fc_state->readLen != 0 &&
		fc_lastReadPage + fc_state->readLen >= fc_wantPage + Min(fc_wantLength, XLOG_BLCKSZ - 1))
		return;

	/*
	 * 如果我们正在从当前时间线读取，它还没有变成历史，
	 * 并且我们正在阅读的页面在最后读取的页面之后，我们可以继续
	 * 进行读取。（向后查找需要检查以确保旧页面不在先前的时间线中）。
	 *
	 * currTLI 可能已经变成历史时间线，因为调用者获取了该值，
	 * 但要求调用者不得超过它在查找时间线时看到的刷新限制。
	 * 如果 StartupXLOG() 同时将其重命名为 .partial，我们对此无能为力。
	 */
	if (fc_state->currTLI == fc_currTLI && fc_wantPage >= fc_lastReadPage)
	{
		Assert(fc_state->currTLIValidUntil == InvalidXLogRecPtr);
		return;
	}

	/*
	 * 如果我们只是从之前已经验证的历史时间线读取页面，
	 * 并且我们正在读取的时间线在当前段的结束之前是有效的，
	 * 我们可以继续读取。
	 */
	if (fc_state->currTLIValidUntil != InvalidXLogRecPtr &&
		fc_state->currTLI != fc_currTLI &&
		fc_state->currTLI != 0 &&
		((fc_wantPage + fc_wantLength) / fc_state->segcxt.ws_segsize) <
		(fc_state->currTLIValidUntil / fc_state->segcxt.ws_segsize))
		return;

	/*
	 * 如果我们到达这一点，我们要么在查找随机访问的页面，
	 * 当前时间线刚刚变成历史，要么我们正在从包含时间线切换的新段中读取。
	 * 在所有情况下，我们需要确定段上最新的时间线。
	 *
	 * 如果它是当前时间线，我们可以在这里继续读取，除非我们检测到
	 * 使当前时间线变成历史的时间线切换。如果它是历史时间线，
	 * 我们可以读取最新时间线上的整个段，因为它也包含所有旧时间线的数据。
	 * 所以只需要进行一次切换检查。
	 */
	{
		/*
		 * 我们需要重新读取时间线历史，以防它被提升或来自级联副本的重放所更改。
		 */
		List	   *fc_timelineHistory = readTimeLineHistory(fc_currTLI);
		XLogRecPtr	fc_endOfSegment;

		fc_endOfSegment = ((fc_wantPage / fc_state->segcxt.ws_segsize) + 1) *
			fc_state->segcxt.ws_segsize - 1;
		Assert(fc_wantPage / fc_state->segcxt.ws_segsize ==
			   fc_endOfSegment / fc_state->segcxt.ws_segsize);

		/*
		 * 查找包含 wantPage 的段上最后一个 LSN 的时间线。
		 */
		fc_state->currTLI = tliOfPointInHistory(fc_endOfSegment, fc_timelineHistory);
		fc_state->currTLIValidUntil = tliSwitchPoint(fc_state->currTLI, fc_timelineHistory,
												  &fc_state->nextTLI);

		Assert(fc_state->currTLIValidUntil == InvalidXLogRecPtr ||
			   fc_wantPage + fc_wantLength < fc_state->currTLIValidUntil);

		list_free_deep(fc_timelineHistory);

		elog(DEBUG3, "switched to timeline %u valid until %X/%X",
			 fc_state->currTLI,
			 LSN_FORMAT_ARGS(fc_state->currTLIValidUntil));
	}
}

/* XLogReaderRoutine->segment_open 本地 pg_wal 文件的回调 */
void wal_segment_open(XLogReaderState *fc_state, XLogSegNo fc_nextSegNo,
				 TimeLineID *fc_tli_p)
{
	TimeLineID	fc_tli = *fc_tli_p;
	char		fc_path[MAXPGPATH];

	XLogFilePath(fc_path, fc_tli, fc_nextSegNo, fc_state->segcxt.ws_segsize);
	fc_state->seg.ws_file = BasicOpenFile(fc_path, O_RDONLY | PG_BINARY);
	if (fc_state->seg.ws_file >= 0)
		return;

	if (errno == ENOENT)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("requested WAL segment %s has already been removed",
						fc_path)));
	else
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not open file \"%s\": %m",
						fc_path)));
}

/* stock XLogReaderRoutine->segment_close 回调 */
void wal_segment_close(XLogReaderState *fc_state)
{
	close(fc_state->seg.ws_file);
	/* 需要检查 errno？ */
	fc_state->seg.ws_file = -1;
}

/*
 * XLogReaderRoutine->page_read 回调，用于读取本地 xlog 文件
 *
 * 公共因为这对于在 walsender 之外编写其他输出方法的人可能非常有帮助，
 * 例如，在一个 bgworker 中。
 *
 * TODO: walsender 有自己版本，但依赖于每当 WAL 被刷新时设置 walsender 的闩锁。
 * 普通后端没有这样的基础设施，因此我们必须暂时采用检查/休眠/重复的循环方式。
 */
int read_local_xlog_page(XLogReaderState *fc_state, XLogRecPtr fc_targetPagePtr,
					 int fc_reqLen, XLogRecPtr fc_targetRecPtr, char *fc_cur_page)
{
	return fc_read_local_xlog_page_guts(fc_state, fc_targetPagePtr, fc_reqLen,
									 fc_targetRecPtr, fc_cur_page, true);
}

/*
 * 与 read_local_xlog_page 相同，只是它不等待未来的 WAL 可用。
 */
int read_local_xlog_page_no_wait(XLogReaderState *fc_state, XLogRecPtr fc_targetPagePtr,
							 int fc_reqLen, XLogRecPtr fc_targetRecPtr,
							 char *fc_cur_page)
{
	return fc_read_local_xlog_page_guts(fc_state, fc_targetPagePtr, fc_reqLen,
									 fc_targetRecPtr, fc_cur_page, false);
}

/*
 * read_local_xlog_page 及其无等待版本的实现。
 */
static int fc_read_local_xlog_page_guts(XLogReaderState *fc_state, XLogRecPtr fc_targetPagePtr,
						  int fc_reqLen, XLogRecPtr fc_targetRecPtr,
						  char *fc_cur_page, bool fc_wait_for_wal)
{
	XLogRecPtr	fc_read_upto,
				fc_loc;
	TimeLineID	fc_tli;
	int			fc_count;
	WALReadError fc_errinfo;
	TimeLineID	fc_currTLI;

	fc_loc = fc_targetPagePtr + fc_reqLen;

	/* 循环等待 xlog 如果有必要的话可用 */
	while (1)
	{
		/*
		 * 确定我们当前可以读取的 xlog 的限制，以及最新的时间线是什么。
		 */
		if (!RecoveryInProgress())
			fc_read_upto = GetFlushRecPtr(&fc_currTLI);
		else
			fc_read_upto = GetXLogReplayRecPtr(&fc_currTLI);
		fc_tli = fc_currTLI;

		/*
		 * 检查从哪个时间线获取记录。
		 *
		 * 我们必须在每次循环中进行此操作，因为如果我们在
		 * 恢复模式下作为级联备用，当前时间线可能已经变成历史时间线。
		 * 我们不能依赖 RecoveryInProgress()，因为在像
		 *
		 * A => B => C
		 *
		 * 这样的备用配置中，如果我们在 C 上进行逻辑解码会话，
		 * 而 B 被提升，那么我们的时间线将会变化，而我们仍然处于恢复状态。
		 *
		 * 我们不能继续从旧的时间线读取，因为在时间线中的最后 WAL
		 * 存档将会被 StartupXLOG() 重命名为 .partial。
		 *
		 * 如果发生这种情况在我们的调用者确定了 TLI 但在我们
		 * 实际读取 xlog 页之前，我们可能仍然会尝试从旧（现在重命名的）
		 * 段读取并失败。对此我们无能为力，但这只能发生在我们是
		 * 级联备用的叶节点，而主节点在我们解码时被提升，因此
		 * 一次性错误也不是太糟。
		 */
		XLogReadDetermineTimeline(fc_state, fc_targetPagePtr, fc_reqLen, fc_tli);

		if (fc_state->currTLI == fc_currTLI)
		{

			if (fc_loc <= fc_read_upto)
				break;

			/* 如果有请求，暂时不等待未来的 WAL。 */
			if (!fc_wait_for_wal)
			{
				ReadLocalXLogPageNoWaitPrivate *fc_private_data;

				/*
				 * 通知调用者 read_local_xlog_page_no_wait 已经到达
				 * WAL 的末尾。
				 */
				fc_private_data = (ReadLocalXLogPageNoWaitPrivate *)
					fc_state->private_data;
				fc_private_data->end_of_wal = true;
				break;
			}

			CHECK_FOR_INTERRUPTS();
			pg_usleep(1000L);
		}
		else
		{
			/*
			 * 我们处于历史时间线，因此限制读取到我们转移到下一个
			 * 时间线的切换点。
			 *
			 * 我们不需要 GetFlushRecPtr 或 GetXLogReplayRecPtr。
			 * 我们知道新的时间线，因此我们必须已经读取过去的最后。
			 */
			fc_read_upto = fc_state->currTLIValidUntil;

			/*
			 * 将 tli 设置为我们想要的记录的 TLI 是稍微错误的；
			 * 如果它包含时间线切换，则页面可能会开始于较旧的时间线，
			 * 因为它的 xlog 段将是从先前的时间线复制的。不过这基本上
			 * 是无害的，只要时间线不向后移动，没人会在意。我们应该
			 * 读取页面头而不是读取；FIXME 以后再说。
			 */
			fc_tli = fc_state->currTLI;

			/* 在历史时间线中无需等待 */
			break;
		}
	}

	if (fc_targetPagePtr + XLOG_BLCKSZ <= fc_read_upto)
	{
		/*
		 * 有多个块可用；只读取该块，如果调用者需要更多则返回。
		 */
		fc_count = XLOG_BLCKSZ;
	}
	else if (fc_targetPagePtr + fc_reqLen > fc_read_upto)
	{
		/* 数据不够 */
		return -1;
	}
	else
	{
		/* 可用的字节足以满足请求 */
		fc_count = fc_read_upto - fc_targetPagePtr;
	}

	/*
	 * 即使我们刚刚确定了可以有效读取的页面数量作为 'count'，
	 * 仍然要读取整个页面。如果页面不完整，它保证会填充零到
	 * 页面边界。
	 */
	if (!WALRead(fc_state, fc_cur_page, fc_targetPagePtr, XLOG_BLCKSZ, fc_tli,
				 &fc_errinfo))
		WALReadRaiseError(&fc_errinfo);

	/* 缓冲区中有效字节的数量 */
	return fc_count;
}

/*
 * 针对 WALRead() 遇到的读取错误的后端特定便利代码。
 */
void WALReadRaiseError(WALReadError *fc_errinfo)
{
	WALOpenSegment *fc_seg = &fc_errinfo->wre_seg;
	char		fc_fname[MAXFNAMELEN];

	XLogFileName(fc_fname, fc_seg->ws_tli, fc_seg->ws_segno, wal_segment_size);

	if (fc_errinfo->wre_read < 0)
	{
		errno = fc_errinfo->wre_errno;
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not read from log segment %s, offset %d: %m",
						fc_fname, fc_errinfo->wre_off)));
	}
	else if (fc_errinfo->wre_read == 0)
	{
		ereport(ERROR,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg("could not read from log segment %s, offset %d: read %d of %d",
						fc_fname, fc_errinfo->wre_off, fc_errinfo->wre_read,
						fc_errinfo->wre_req)));
	}
}
